/*
 * Copyright (C) 2014 Nathan Templon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package unit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.TreeMap;
import log.Logger;

/**
 *
 * @author Nathan Templon
 */
public class DerivedUnit implements Unit, Comparable {
    
    public DerivedUnit(String name, double scale, UnitComponent[] components) {
        logger = Logger.getDefaultLogger();
        this.name = name;
        this.scaleToBase = scale;
        this.scale = scale;
        this.components = components;
        init();
    }
    
    
    public DerivedUnit(double scale, UnitComponent[] components) {
        logger = Logger.getDefaultLogger();
        this.name = null;
        this.scaleToBase = scale;
        this.scale = scale;
        this.components = components;
        init();
    }
    
    
    private void init() {
        
        // Load powerMap
        powerMap = new TreeMap<>();
        
        for( UnitComponent comp : components ) {
            Unit unit = comp.unit();
            TreeMap<UnitType, Integer> powMap = unit.powerMap();
            
            for( UnitType type : powMap.keySet() ) {
                if (powerMap.containsKey(type)) {
                    int newPower = powerMap.get(type) + comp.pow() * powMap.get(type);
                    powerMap.remove(type);
                    powerMap.put(type, newPower);
                }
                else {
                    powerMap.put(type, comp.pow() * powMap.get(type));
                }
            }
            
        }
        
        Collection<UnitType> zeroPowerTypes = new ArrayList<>();
        for( UnitType type : powerMap.keySet() ) {
            if( powerMap.get(type) == 0 ) {
                zeroPowerTypes.add(type);
            }
        }
        for( UnitType type : zeroPowerTypes ) {
            powerMap.remove(type);
        }
        
        // Get the scaleToBase
        for( UnitComponent comp : components ) {
            this.scaleToBase = this.scaleToBase * Math.pow(comp.unit().scaleToBase(), comp.pow());
        }
    }
    
    
    @Override
    public TreeMap<UnitType, Integer> powerMap() {
        return powerMap;
    }
    
    
    /**
     * A method for getting the scale of the unit.  This number is how many base units it takes to equal one of this unit.
     * @return Returns the scale of the unit.
     */
    @Override
    public double scaleToBase() {
        return scaleToBase;
    }
    
    
    @Override
    public String name() {
        if (name != null) {
            return name;
        }
        else {
            return toString();
        }
    }
    
    
    public UnitComponent[] components() {
        return components;
    }
    
    
    @Override
    public double scale() {
        return scale;
    }
    
    
    @Override
    public double scaleTo(Unit other) {
        if(isSimilar(other)) {
            return scaleToBase / other.scaleToBase();
        }
        else {
            return 0;
        }
    }
    
    
    @Override
    public boolean isSimilar(Unit other) {
        Collection<UnitType> otherTypes = other.powerMap().keySet();
        if (!otherTypes.equals(powerMap.keySet())) {
            return false;
        }
        for(UnitType type : otherTypes) {
            if(!other.powerMap().get(type).equals(powerMap.get(type))) {
                return false;
            }
        }
        
        return true;
    }
    
    
    @Override
    public Unit times(Unit other) {
        TreeMap<Unit, Integer> compMap = new TreeMap<>();
        TreeMap<Unit, Integer> otherCompMap = new TreeMap<>();
        TreeMap<Unit, Integer> newCompMap = new TreeMap<>();
        
        UnitComponent[] otherComps = other.components();
        
        for(UnitComponent comp : components) {
            compMap.put(comp.unit(), comp.pow());
        }
        for(UnitComponent otherComp : otherComps) {
            otherCompMap.put(otherComp.unit(), otherComp.pow());
        }
        
        for(Unit unit : compMap.keySet()) {
            if (otherCompMap.containsKey(unit)) {
                newCompMap.put(unit, otherCompMap.get(unit) + compMap.get(unit));
            }
            else {
                newCompMap.put(unit, compMap.get(unit));
            }
        }
        
        for (Unit unit : otherCompMap.keySet()) {
            if (!newCompMap.containsKey(unit)) {
                newCompMap.put(unit, otherCompMap.get(unit));
            }
        }
        
        double newScale = this.scale * other.scale();
        
        UnitComponent[] newComps = new UnitComponent[newCompMap.keySet().size()];
        int index = 0;
        for(Unit unit : newCompMap.keySet()) {
            newComps[index] = new UnitComponent(unit, newCompMap.get(unit));
            index++;
        }
        
        return new DerivedUnit(newScale, newComps);
    }
    
    
    @Override
    public Unit dividedBy(Unit other) {
        TreeMap<Unit, Integer> compMap = new TreeMap<>();
        TreeMap<Unit, Integer> otherCompMap = new TreeMap<>();
        TreeMap<Unit, Integer> newCompMap = new TreeMap<>();
        
        UnitComponent[] otherComps = other.components();
        
        for(UnitComponent comp : components) {
            compMap.put(comp.unit(), comp.pow());
        }
        for(UnitComponent otherComp : otherComps) {
            otherCompMap.put(otherComp.unit(), otherComp.pow());
        }
        
        for(Unit unit : compMap.keySet()) {
            if (otherCompMap.containsKey(unit)) {
                newCompMap.put(unit, (-1 *  otherCompMap.get(unit) ) + compMap.get(unit));
            }
            else {
                newCompMap.put(unit, compMap.get(unit));
            }
        }
        
        for (Unit unit : otherCompMap.keySet()) {
            if (!newCompMap.containsKey(unit)) {
                newCompMap.put(unit, -1 * otherCompMap.get(unit));
            }
        }
        
        double newScale = this.scale / other.scale();
        
        UnitComponent[] newComps = new UnitComponent[newCompMap.keySet().size()];
        int index = 0;
        for(Unit unit : newCompMap.keySet()) {
            newComps[index] = new UnitComponent(unit, newCompMap.get(unit));
            index++;
        }
        
        return new DerivedUnit(newScale, newComps);
    }
    
    
    @Override
    public Unit compressToBase() {
        TreeMap<BaseUnit, Integer> baseMap = new TreeMap<>();
        
        for(UnitComponent comp : components) {
            baseMap = mergeCompBaseMaps(baseMap, getCompBaseMap(comp));
        }
        
        UnitComponent[] comps = new UnitComponent[baseMap.keySet().size()];
        int index = 0;
        for(BaseUnit base : baseMap.keySet()) {
            comps[index] = new UnitComponent(base, baseMap.get(base));
            index++;
        }
        return new DerivedUnit(scaleToBase, comps);
    }
    
    
    @Override
    public Unit scaleless() {
        return new DerivedUnit(1.0, components);
    }
    
    
    private TreeMap<BaseUnit, Integer> getCompBaseMap(UnitComponent comp) {
        TreeMap<BaseUnit, Integer> returnMap = new TreeMap<>();
        Unit unit = comp.unit();
        
        if (unit instanceof BaseUnit) {
            returnMap.put((BaseUnit)unit, comp.pow());
        }
        else if (unit instanceof DerivedUnit) {
            for(UnitComponent otherComp : unit.components()) {
                returnMap = mergeCompBaseMaps(returnMap, getCompBaseMap(otherComp));
            }
        }
        else {
            throw new UnsupportedOperationException("Only BaseUnit and DerivedUnit are supported.");
        }
        
        return returnMap;
    }
    
    
    private TreeMap<BaseUnit, Integer> mergeCompBaseMaps(TreeMap<BaseUnit, Integer> map1, TreeMap<BaseUnit, Integer> map2) {
        TreeMap<BaseUnit, Integer> returnMap = new TreeMap<>();
        Collection<BaseUnit> zeroPowerUnits = new ArrayList<>();
        for (BaseUnit unit : map1.keySet()) {
            if (map2.containsKey(unit)) {
                int power = map1.get(unit) + map2.get(unit);
                if (power != 0) {
                    returnMap.put(unit, map1.get(unit) + map2.get(unit));
                }
                else {
                    zeroPowerUnits.add(unit);
                }
            }
            else {
                returnMap.put(unit, map1.get(unit));
            }
        }
        for (BaseUnit unit : map2.keySet()) {
            if (!returnMap.containsKey(unit) && ! zeroPowerUnits.contains(unit)) {
                returnMap.put(unit, map2.get(unit));
            }
        }
        return returnMap;
    }
    
    
    @Override
    public String toString() {
        String output = "";
        
        if (!(Math.abs(scale - 1) < 10E-7)) {
            output += scale + " * ";
        }
        
        int index = 0;
        for (UnitComponent component : components) {
            
            output += component.unit().name() + "^" + component.pow();
            
            if (index < (components.length - 1)) {
                output += " * ";
            }
            
            index++;
        }
        
        return output;
    }
    
    
    @Override
    public int compareTo(Object o) {
        // The position that we are relative to the provided object
        final int BEFORE = -1;
        final int EQUAL = 0;
        final int AFTER = 1;
        
        if (this.equals(o)) {
            return EQUAL;
        }
        
        if (o instanceof Unit) {
            Unit newO = (Unit)o;
            return this.name().compareTo(newO.name());
        }
        
        // If they didn't send us a UnitType, there isn't much else we can do
        return EQUAL;
    }
    
    
    private boolean arrayContainsUnit(UnitComponent[] units, Unit checkFor) {
        for(UnitComponent unitComp : units) {
            if (unitComp.unit().equals(checkFor)) {
                return true;
            }
        }
        return false;
    }
    
    
    private final Logger logger;
    
    private final String name;
    private final double scale;
    private double scaleToBase;
    private final UnitComponent[] components;
    
    private TreeMap<UnitType, Integer> powerMap;
    
    
    public static class UnitComponent {
        
        public UnitComponent( Unit unit, int pow ) {
            this.unit = unit;
            this.pow = pow;
        }
        
        
        public Unit unit() {
            return unit;
        }
        
        
        public int pow() {
            return pow;
        }
        
        @Override
        public String toString() {
            return unit.toString() + "^" + pow;
        }
        
        
        
        private final Unit unit;
        private final int pow;
        
    }
}
